1장. 결합은 적이 아니라 접착제다
출처: 『소프트웨어 설계의 결합 균형』(블라드 코노노프 지음, 제이펍 2026) | 원서: Balancing Coupling in Software Design (Manning) · 입문판·PDF 재구성 · 원서 1장 대응
코드는 분위기만 —
class·this·ExecuteSql같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.
0. 이 장의 새 단어 (0장에 없는 것만)
0장 용어집에 있는 말(결합·공유 지식·상위/하위·캡슐화 경계 등)은 거기서 보면 된다.
여기선 1장에서 새로 나오는 말 3개만 미리 풀어 둔다.
어원 copulare (코풀라레)
한 문장 뜻 — '결합(coupling)'이라는 영어 단어가 태어난 라틴어. co(함께) + apere(고정) = "함께 고정한다".
일상비유 — '커플(couple)'이랑 형제 단어다. 두 사람을 묶어 한 쌍으로 만든다는 그 느낌.
한 줄 예 —
// coupling = co(함께) + apere(고정) → "묶다"
var two_things_joined = true; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
허용 오차 (Tolerance)
한 문장 뜻 — 부품을 연결할 때 봐주는 오차의 폭. 딱 맞지 않아도 끼워지게 두는 여유.
일상비유 — 콘센트 구멍. 플러그가 머리카락 하나 굵기까지 똑같아야 들어간다면 아무것도 못 꽂는다. 살짝 여유가 있어 잘 꽂힌다.
한 줄 예 —
// 여유 0이면 못 끼우고, 여유 과하면 흔들린다
var gap = 0.1; // 적당한 여유 = 적당한 허용 오차
프랙털(fractal) 구조
한 문장 뜻 — 큰 걸 확대해도 작은 게 똑같은 모양으로 또 나오는 구조. 시스템 안에 또 시스템이 있는 형태.
일상비유 — 양파. 까도 까도 똑같이 생긴 껍질이 또 나온다. 회사 안에 서비스, 서비스 안에 클래스, 그 안에 또 작은 부품.
한 줄 예 —
var 회사 = [서비스1, 서비스2]; // 서비스도 그 안에선 또 하나의 시스템
var 서비스1 = [클래스A, 클래스B]; // 클래스도 그 안에선 또 하나의 시스템
(귀납 도입) 이런 적 있죠?
"결합"이라는 말, 욕처럼 쓴 적 있을 거다.
코드가 안 고쳐지고 자꾸 딴 데가 터지면 "아 결합이 심해서 그래"라고 한다.
그래서 우리는 본능적으로 다 떼어 놓으려 한다. 클래스도, 모듈도, 서비스도 잘게 쪼갠다.
그런데 잠깐. 결합이 진짜 0인 시스템을 상상해 보자.
// 모든 부품이 서로 완전히 모른다
var order = new Order(); // 결제를 못 부름
var payment = new Payment(); // 주문을 모름
// → 둘이 연결이 없으니 "결제된 주문"을 만들 수가 없다
부품들이 서로 아무 연결이 없다.
그럼 이게 좋은 설계일까?
아니다. 이건 시스템이 아니라 그냥 부품 더미다.
여기서 이 장의 큰 깨달음이 나온다.
결합은 없애야 할 적이 아니다. 시스템을 묶어 주는 접착제다.
진짜 질문은 "결합이 있냐 없냐"가 아니라 "어떤 결합이 얼마나 있냐"다.
이 장에서 딱 5가지만
TL;DR
- 결합 = 연결이다. '결합됐다'를 '연결됐다'로 바꿔 읽으면 된다. 나쁜 말이 아니다.
- 결합이 강한지 약한지는 두 가지로 정해진다 — 같이 배포되는가(수명주기), 서로 얼마나 아는가(지식).
- 지식은 한 방향으로 흐른다 — 기능을 내주는 상위에서, 받아 쓰는 하위로. 그래서 상위를 바꾸면 위험하다.
- 시스템 = 구성요소 + 상호작용 + 목적. 이 셋은 묶여 있어 하나를 바꾸면 나머지도 따라온다. 결합은 그중 '상호작용'이다.
- 결합엔 꼭 필요한 것(필수)과 얼떨결에 생긴 것(우연)이 있다. 우연만 지운다.
1. 결합 = 연결 (어원에서 본질로)
망가지는 장면
함수 하나 고쳤을 뿐인데 전혀 딴 데가 터진 적 있죠?
"여기 만졌는데 왜 저기가 죽지?"
그게 둘이 연결돼 있다는 신호다. 그 연결이 바로 결합이다.
비유 먼저 — '결합됨'을 '연결됨'으로 바꿔 읽기
결합이라는 말이 어려우면 한 단어만 기억하면 된다.
'결합됨'을 만나면 '연결됨'으로 바꿔 읽어라.
"데이터베이스에 강하게 결합된 객체" = "데이터베이스에 강하게 연결된 객체".
뜻이 그대로 통한다.
이 단어는 라틴어 copulare(co=함께 + apere=고정), 즉 "함께 고정한다"에서 왔다.
그래서 결합은 소프트웨어만의 말이 아니다. 세상 거의 모든 게 결합돼 있다.
- 시계 — 기어와 스프링이 연결돼 시간을 잰다.
- 자동차 — 엔진·바퀴·브레이크가 연결돼 굴러간다.
- 사람 몸 — 장기들이 연결돼 살아 있는 몸이 된다.
- 우주 — 별들은 멀리 떨어져 있어도 중력으로 연결돼 있다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 맞잡은 손(연결됨) | service.repo = mysql_repo |
한쪽이 바뀌면 다른 쪽이 끌려옴 |
| 손 안 잡음(연결 0) | 서로 호출 없음 | 협력 불가 = 시스템이 아님 |
한 문장 정의 — 결합은 두 부품이 연결돼 서로 영향을 주는 관계이며, 없애야 할 적이 아니라 시스템을 묶어 주는 접착제다.
예시 폭격
예시 1 (worked — 완성예). '결합'을 '연결'로 바꿔 읽기
// "주문 객체가 결제 서비스에 결합됨"
order.PaymentService = payment; // = "주문이 결제에 연결됨"
// 결제 인터페이스가 바뀌면 주문도 영향받는다 (연결돼 있으니까)
예시 2 (부분완성 — 빈칸 채우기). 아래 문장의 '결합됨'을 '연결됨'으로 바꿔 보자.
"이 화면 코드는 DB 테이블에 강하게 결합돼 있다."
↓ (당신이 바꿔 읽기)
"이 화면 코드는 DB 테이블에 강하게 ______ 있다."
(정답: 연결돼)
예시 3 (독립적용). 일상에서 결합 찾기 — 자전거 페달과 바퀴는 체인으로 연결(결합)돼 있다. 페달을 밟으면 바퀴가 돈다. 체인을 끊으면? 페달은 헛돌고 자전거는 안 굴러간다. 연결 0 = 자전거가 아니다.
미니 시나리오 — 동료가 "이 모듈 결합 좀 줄이자"라고 한다. 머릿속에서 "이 모듈 연결 좀 줄이자"로 바꿔 들어 보라. 그럼 "어떤 연결을? 다 끊으면 안 돌 텐데?"라는 진짜 질문이 떠오른다.
2. 결합의 규모 — 무엇이 결합을 강하게 만드나
망가지는 장면
"왜 작은 수정 하나에 전체를 다시 배포하지?"
"왜 DB만 바꾸려는데 화면 코드까지 손대야 하지?"
앞은 같이 배포되는 문제, 뒤는 서로 너무 많이 아는 문제다.
결합이 강하다 = 같이 변경해야 하는 빈도가 높다. 그 빈도를 끌어올리는 동인이 딱 두 개다.
비유 먼저 — 부부와 가끔 보는 친구
같이 배포되는 정도(수명주기)는 한집 사는 부부 같다. 이사 한 번에 둘 다 짐 싼다.
서로 아는 양(지식)은 부탁의 무게 같다. 전화번호만 알면 가볍고, 집 구조·일정까지 알아야 하면 무겁다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 한집 부부(수명주기) | from .payments import charge |
한쪽 고치면 전체 재배포 |
| 집 구조까지 앎(지식 많음) | repo.ExecuteSql("INSERT ...") |
DB 바꾸면 호출부도 깨짐 |
| 전화번호만 앎(지식 적음) | repo.Save(entity) |
거의 안 깨짐 |
한 문장 정의 — 결합의 규모는 함께 변경되는 빈도이며, 그 빈도는 공유 수명주기와 공유 지식 두 가지가 정한다.
2.1 공유 수명주기 — 같이 빌드·테스트·배포되는가
같은 한 덩어리(모놀리식) 안에 모듈 둘을 넣으면, 한 곳을 고쳐도 전체를 다시 빌드·테스트·배포한다.
별도 서비스로 떼어 놓으면 각자 따로 배포할 수 있다.
// A. 한 덩어리 — 수명주기 결합 강함
// 결제(Payments)와 인증(Authorization)이 같은 모놀리식에 있음
// using Authorization; // check 기능을 같은 배포 단위에서 가져온다는 뜻입니다.
// B. 서비스 분리 — 수명주기 결합 약함
// 결제는 Billing 서비스, 인증은 Identity & Access 서비스로 분리
var billing_url = "http://billing:5000/charge"; // 결제는 따로 도는 별도 서버
A는 결제만 고쳐도 인증까지 같이 테스트·배포된다.
B는 결제 서비스만 따로 배포한다. 인증은 멀쩡히 돌고 있다.
한 걸음 더 ▸ (지금 몰라도 됨) 캡슐화 경계 말고도 팀 구조·배포 파이프라인 같은 게 수명주기 결합에 영향을 준다. 이 "거리"라는 주제는 나중에 풀린다. 지금은 "같은 덩어리 = 같이 배포 = 강한 결합"만 들고 가면 된다.
2.2 공유 지식 — 서로 얼마나 알아야 하나
같은 일(고객 저장)을 세 가지로 설계해 보자. 서로 아는 양이 점점 줄어든다.
A. 가장 많이 안다 — DB 제품 이름까지 노출
// CustomersService 가 "MySQL"이라는 구체 제품을 안다
public class CustomersService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public CustomersService(dynamic repo: MySQLRepository) // 생성자: 이 객체가 어떤 협력 객체를 기억할지 정합니다.
{
this.Repo = repo; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
}
public dynamic Register(dynamic customer) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
this.Repo.ExecuteSql("INSERT INTO customers /* 구현 생략 */"); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}
문제 — MySQL을 다른 DB로 바꾸려면 CustomersService 코드까지 손대야 한다. DB 선택이라는 지식이 밖으로 새어 나왔다.
B. 중간 — DB 제품은 숨겼지만 "SQL"은 여전히 안다
public class IRepository // 저장 기능의 약속입니다. 호출자는 구체 DB를 몰라도 됩니다.
{
public void ExecuteSql(string query, object params) { /* 구현 생략 */; } // SQL 가정이 남음
}
public class CustomersService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public CustomersService(dynamic repo: IRepository) // 생성자: 이 객체가 어떤 협력 객체를 기억할지 정합니다.
{
this.Repo = repo; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
}
public dynamic Register(dynamic customer) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
this.Repo.ExecuteSql("INSERT INTO customers /* 구현 생략 */"); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}
진전 — 구체 제품은 모른다. 하지만 "관계형 DB 집단"은 여전히 가정한다. 키-값 저장소로 바꾸면 깨진다.
C. 가장 적게 안다 — 의도(저장한다)만 노출
public class IRepository // 저장 기능의 약속입니다. 호출자는 구체 DB를 몰라도 됩니다.
{
public void Save(object entity) { /* 구현 생략 */; } // 어떻게 저장하는지 모름
public void Query(string criteria) { /* 구현 생략 */; } // 어떻게 검색하는지 모름
}
public class CustomersService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public CustomersService(dynamic repo: IRepository) // 생성자: 이 객체가 어떤 협력 객체를 기억할지 정합니다.
{
this.Repo = repo; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
}
public dynamic Register(dynamic customer) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
this.Repo.Save(customer); // "저장이 필요하다"는 의도뿐
}
}
결과 — 저장소가 MySQL이든 메모리든 CustomersService 입장에선 똑같다. 공유되는 지식은 "고객을 저장한다"는 의도뿐이다.
| 단계 | CustomersService가 아는 것 | DB 바꾸면 |
|---|---|---|
| A | DB 제품(MySQL) + SQL | 깨짐 |
| B | DB 집단(관계형) + SQL | NoSQL이면 깨짐 |
| C | "저장한다"는 의도뿐 | 거의 안 깨짐 |
A에서 C로 갈수록 서로 아는 게 줄고, 그만큼 변경이 덜 번진다.
한 걸음 더 ▸ (지금 몰라도 됨) 그렇다고 무조건 C가 정답은 아니다. 너무 추상화하면 정작 필요한 정보(예: 거래 경계 통제)까지 가려진다. 적정선을 고르는 게 설계인데, 이건 더 깊은 주제다. 지금은 "적게 알수록 안전"만.
2.3 암묵적 지식 — 더 위험한 형태
명시적으로 공유 안 했는데도 말없이 가정하는 지식이 가장 위험하다.
process_payment(req); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
// 숨은 가정 1: "운영체제는 Linux일 거야" → Windows에서 폭발
// 숨은 가정 2: "이 API는 5초 안에 응답해" → 네트워크 지연에서 폭발
// 숨은 가정 3: "이 큐는 절대 안 비어 있어" → 새벽 트래픽에서 폭발
실무 체크 — 코드 리뷰에서 "이건 항상 X일 거야"가 들리면, 그 가정이 약속(인터페이스)에 적혀 있는지 확인하라. 안 적혀 있으면 그게 암묵적 결합이다.
예시 폭격
예시 1 (worked). 위 A→B→C가 곧 완성예다. 같은 register가 아는 양만 줄어든다.
예시 2 (부분완성). 아래 코드는 A·B·C 중 어디일까?
public class CartService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public CartService(dynamic repo: IRepository) // 생성자: 이 객체가 어떤 협력 객체를 기억할지 정합니다.
{
this.Repo = repo; // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
}
public dynamic Add(dynamic item) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
this.Repo.ExecuteSql("INSERT INTO cart /* 구현 생략 */"); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}
(정답: B — 제품 이름은 숨겼지만 execute_sql로 SQL을 안다)
예시 3 (독립적용). 친구에게 "택배 좀 받아 줘"라고 부탁한다. 우리집 비밀번호·방 구조까지 알려주면(지식 많음) 부담이고 위험하다. 경비실에 맡겨 달라고만 하면(의도만) 가볍다. 같은 부탁, 다른 공유 지식.
미니 시나리오 — DB를 MySQL에서 다른 걸로 바꿔야 한다. 코드가 A 단계면 서비스 코드를 다 뒤져야 한다. C 단계면 저장소 한 곳만 갈아 끼우면 끝난다. 미래의 당신이 고마워할 일이다.
3. 지식의 흐름 — 상위와 하위
망가지는 장면
상위 쪽 인터페이스를 살짝 바꿨더니, 그걸 쓰던 하위 코드들이 조용히 다 깨졌다.
하위는 자기가 깨지는 줄도 몰랐다. 왜? 지식이 한 방향으로만 흐르기 때문이다.
비유 먼저 — 식당과 손님
식당(상위)이 메뉴를 내준다. 손님(하위)이 그 메뉴를 보고 주문한다.
메뉴가 바뀌면 손님은 알아야 한다(식당 → 손님). 하지만 손님이 바뀌어도 식당은 모른다.
흐름은 한 방향이다.
price_service.GetPrice(item_id); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
// price_service = 상위(기능 제공)
// 이 코드 = 하위(기능 소비)
// 하위가 상위의 메서드 이름·입력을 "알아야" 부를 수 있다
재밌는 비대칭이 있다. 부르는 화살표(종속성)는 하위 → 상위인데, 아는 화살표(지식 흐름)는 상위 → 하위로 반대다.
하위가 상위를 알아야 부를 수 있으니까.
| 비유 | 코드 | 위험 |
|---|---|---|
| 식당 = 상위(제공) | price_service.get_price(id) |
상위가 바뀌면 하위가 깨짐 |
| 손님 = 하위(소비) | 이 코드가 price_service를 호출 | 손님이 바뀌어도 식당은 무사 |
한 문장 정의 — 기능을 내주는 상위와 받아 쓰는 하위 사이에서 지식은 상위→하위 한 방향으로 흐르므로, 상위 인터페이스 변경이 가장 위험하다.
예시 폭격
예시 1 (worked). Distribution 모듈이 CRM 모듈을 부른다. CRM이 상위(고객 데이터 제공), Distribution이 하위(소비). CRM의 메서드 이름이 바뀌면 Distribution이 깨진다. 그 반대는 없다.
예시 2 (부분완성). 누가 상위이고 누가 하위인가?
report.Build(data_from = analytics_api); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
(정답: analytics_api = 상위(데이터 제공), report = 하위(소비))
예시 3 (독립적용). 콘센트와 충전기. 콘센트(상위)가 전기를 내준다. 충전기(하위)가 받아 쓴다. 콘센트 규격이 바뀌면 충전기를 새로 사야 한다. 충전기를 바꿔도 콘센트는 그대로다.
미니 시나리오 — 공용 API의 응답 형식을 바꾸려 한다. "그걸 쓰는 모든 하위"를 먼저 찾아야 한다. 안 찾으면 어딘가의 하위가 조용히 깨진다.
4. 시스템 = 구성요소 + 상호작용 + 목적
망가지는 장면
"새 기능 하나만 넣으면 되는데, 왜 모듈도 새로 만들고 호출도 다 바꿔야 하지?"
기능(목적)을 건드렸을 뿐인데 부품(구성요소)도, 연결(상호작용)도 줄줄이 따라온다.
세 가지가 묶여 있어서다.
비유 먼저 — 톱니 더미와 시계
시계 톱니가 상자 안에 따로 흩어져 있으면? 그냥 부품 더미다.
톱니들이 맞물려 돌아야(상호작용) "시간을 알려주는 시계"(목적)가 된다.
톱니(구성요소)만 있다고 시계가 되는 게 아니다. 맞물림이 있어야 한다.
도넬라 메도즈는 시스템을 이렇게 정의했다.
"무언가를 달성하는 방식으로 구성된, 상호 연결된 요소들의 집합."
여기서 시스템의 세 요소가 나온다.
- 구성요소 — 부품(서비스·모듈·클래스·메서드·문장).
- 상호작용 — 부품들이 어떻게 협력하는가. 이게 바로 결합이다.
- 목적 — 왜 협력하는가(비즈니스 가치).
| 비유 | 코드 | 위험 |
|---|---|---|
| 시계 톱니(구성요소) | parts = [order, payment, ship] |
부품만 있고 안 맞물리면 부품 더미 |
| 톱니 맞물림(상호작용=결합) | order.Charge(payment) |
맞물림이 곧 결합 — 시스템을 시스템으로 만듦 |
| 시간 알림(목적) | goal = "주문을 처리한다" |
목적 바꾸면 부품·맞물림 다 따라옴 |
한 문장 정의 — 시스템은 구성요소·상호작용·목적 세 요소가 서로 의존하는 집합이며, 그중 상호작용이 곧 결합이다.
세 요소는 묶여 있다. 하나를 바꾸면 나머지가 영향받는다.
// 목적 확장 → 구성요소 추가 → 상호작용 변경, 줄줄이
var goal = "주문 처리 + 적립 추가"; // ① 목적 변경
var loyalty = new LoyaltyModule(); // ② 새 구성요소 필요
order.AddPoints(loyalty); // ③ 새 상호작용(연결) 생김
루스 맬런의 말 — "시스템 설계는 본질적으로 경계, 그리고 상충 관계에 관한 것이다." 상자(구성요소)만큼 상자 사이의 선(상호작용=결합)도 정교하게 그려야 한다.
시스템 안에 또 시스템 — 프랙털
소프트웨어는 양파 같다. 까도 까도 또 시스템이 나온다.
회사_전체; // 시스템
└ Processor_서비스; // 그 자체로 또 시스템
└ DataAccess_클래스; // 그 자체로 또 시스템
└ next_request_메서드; // 그 자체로 또 시스템
└ if/return 문장들; // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
같은 결합 원리가 모든 층에 똑같이 적용된다.
메서드 안 if 분기끼리도 결합돼 있고, 서비스 사이 메시지 큐도 결합돼 있다.
한 걸음 더 ▸ (지금 몰라도 됨) 이 "양파 구조" 자체가 깊은 주제다. 지금은 "어느 층에서든 결합 원리는 똑같다"만 기억하면 된다.
예시 폭격
예시 1 (worked). 위의 goal = "주문 처리 + 적립"이 완성예다. 목적 한 줄 바꾸니 부품·연결이 따라왔다.
예시 2 (부분완성). 아래에서 어느 게 '구성요소', '상호작용', '목적'인가?
// var cart = new Cart(); var payment = new Payment(); // 여러 값을 한 번에 넣는 C#에서는 개별 대입으로 풀어 쓰는 표현은 C#에서 개별 대입으로 풀어 씁니다.
cart.Checkout(payment); // (나)
var goal = "장바구니를 결제한다"; // (다)
(정답: 가=구성요소, 나=상호작용, 다=목적)
예시 3 (독립적용). 축구팀. 선수(구성요소)만 11명 있다고 팀이 아니다. 패스·수비 호흡(상호작용)이 맞아야 "이긴다"(목적)는 시스템이 된다.
미니 시나리오 — PM이 "기능 하나만 추가"라고 가볍게 말한다. 목적을 건드리면 부품과 연결이 따라온다는 걸 알면, "이 한 줄이 어디까지 번지는지"를 미리 짚을 수 있다.
5. 필수 결합 vs 우연 결합 — 결합은 도구다
망가지는 장면
"의존성은 무조건 줄여야 좋다"며 다 끊으려다, 정작 꼭 필요한 연결(주문→결제)까지 건드려 시스템이 멈췄다.
연결엔 꼭 필요한 것과 없어도 될 것이 섞여 있는데 구분을 안 한 탓이다.
비유 먼저 — 엔진 연결 vs 커피 자국
자동차에 엔진과 바퀴가 연결된 건 필수다.
운전대에 묻은 커피 자국은 우연이다. 없어야 좋다.
지워야 할 건 커피 자국이지 엔진 연결이 아니다.
| 구분 | 비유 | 코드 | 처방 |
|---|---|---|---|
| 필수 결합 | 엔진-바퀴 | order.Charge(payment) |
신중하게 관리 |
| 우연 결합 | 커피 자국 | handler.db.execute("...col") |
제거 |
한 문장 정의 — 결합은 목적상 꼭 필요한 필수 결합과 부주의로 생긴 우연 결합으로 나뉘며, 제거 대상은 우연 결합뿐이다.
흔한 오해 하나 짚자. "모든 의존성을 0으로." 불가능하고 무용하다.
목표는 결합 제거가 아니라 결합 질의 제어다.
예시 폭격
예시 1 (worked — before/after). 우연 결합 한 줄 지우기
// before (우연 결합): 화면 핸들러가 DB 컬럼명을 직접 안다
@app.Post("/orders"); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
static dynamic CreateOrder(dynamic req) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
db.Execute("INSERT INTO orders (user_id, items_json) VALUES (?, ?)",; // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
(req.UserId, json.Dumps(req.Items))); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
// 문제: items_json → items 로 컬럼명만 바꿔도 이 화면이 깨진다
// after (우연 결합 제거): 저장소가 컬럼명을 숨긴다
@app.Post("/orders"); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
static dynamic CreateOrder(dynamic req) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
var order = Order.Create(userId: req.UserId, items: req.Items); // 이 값이나 객체를 알게 되는 순간 결합 지점이 됩니다.
order_repo.Save(order); // 화면은 "저장한다"만 안다
}
// 컬럼명은 order_repo 안에서만 안다 → 스키마 변경이 화면을 안 깨뜨림
예시 2 (부분완성). 다음 연결은 필수일까 우연일까?
ui_button.Db.Execute("UPDATE users SET /* 구현 생략 */"); // 화면 버튼이 DB를 직접 조작
(정답: 우연 — 화면이 DB 속사정을 직접 알 이유가 없다. 제거 대상)
예시 3 (독립적용). 집 배선. 전등 스위치와 전등이 연결된 건 필수. 그런데 화장실 스위치를 누르면 주방 불이 켜진다면? 그건 잘못 얽힌 우연 결합이다. 고쳐야 한다.
미니 시나리오 — 새 의존성을 추가하기 직전에 한 번만 자문하라. "이건 목적상 꼭 필요한 연결인가(필수), 아니면 지금 편해서 끌어 쓰는 연결인가(우연)?" 우연이면 다른 길을 찾는다.
6. (선택) 기계공학의 비유 — 허용 오차
기계공학에서 두 부품을 결합할 때, 절단면을 완벽히 똑같이 만들기는 비싸고 종종 불가능하다.
그래서 일부러 허용 오차 (Tolerance)를 둔다. 약간의 여유다.
- 허용 오차가 너무 작으면 — 작은 결함에도 안 끼워진다(빡빡).
- 허용 오차가 너무 크면 — 헐거워서 흔들린다(느슨).
소프트웨어의 입력 검증도 똑같다.
// 너무 빡빡 — 정상 입력도 거절
static dynamic ParsePhone(dynamic s) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
if s != "010-1234-5678": raise ValueError; // 한 글자만 달라도 거절
}
// 너무 헐거움 — 쓰레기도 통과
static dynamic ParsePhone(dynamic s) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
return s; // "abc"도 그대로 DB까지 간다
}
// 적절한 허용 오차 — 살짝 봐주되 선은 지킴
static dynamic ParsePhone(dynamic s) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
var digits = new string(s.Where(char.IsDigit).ToArray()); // 하이픈·공백을 빼고 숫자만 남김
if len(digits) != 11: raise ValueError; // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
return digits; // 결과를 호출자에게 돌려줍니다.
}
핵심 — 결합부도 인터페이스도, 빡빡하지도 헐겁지도 않은 적절한 여유가 답이다.
단순 규칙
이 장은 결국 한 줄이다.
결합은 적이 아니라 접착제다. 없애지 말고, 어떤 결합을 둘지 고르라.
그리고 0장의 안전판을 그대로 들고 가면 된다.
서로 아는 게 가장 적은 방식이 기본 안전판이다. 적게 알수록 변경이 덜 번진다.
정리
- 결합 = 연결이다. '결합'을 '연결'로 바꿔 읽어라. 0이면 시스템이 아니라 부품 더미다.
- 결합 세기는 수명주기(같이 배포되나)와 지식(서로 얼마나 아나)으로 정해진다. 적게 알수록 안전.
- 지식은 상위→하위 한 방향으로 흐른다. 상위를 바꾸면 하위가 조용히 깨진다.
- 시스템 = 구성요소 + 상호작용 + 목적. 셋은 묶여 있고, 상호작용이 곧 결합이다.
- 결합엔 필수와 우연이 있다. 우연만 지운다. 목표는 제거가 아니라 질의 제어.
다음 2장 예고 — 그럼 코드베이스를 망치는 진짜 힘은 뭘까? '복잡성'이다. (지금 몰라도 됩니다 — 2장에서 풀려요.)
연습문제
- 설명.
결합은 적이 아니라 접착제다의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라. - 구분. 두 개념(
필수 결합,우연 결합)을 실제 예시 하나로 구분하라. - 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.
부록 A. 쉬운 용어 사전
| 한글 용어 | 원문 영문명 | 아주 쉬운 뜻 | 이 장에서 나온 위치 |
|---|---|---|---|
| 필수 결합 | Essential Coupling | 시스템 목적상 끊으면 안 되는 필요한 연결. | 부록 B와 본문 예시 |
| 우연 결합 | Accidental Coupling | 목적상 필요 없는데 설계나 구현 편의 때문에 생긴 연결. | 부록 B와 본문 예시 |
| 공유 수명주기 | Shared Lifecycle | 두 부품이 얼마나 함께 빌드, 테스트, 배포되는지. | 부록 B와 본문 예시 |
| 공유 지식 | Shared Knowledge | 한 부품이 다른 부품의 속사정을 얼마나 알아야 하는지. | 부록 B와 본문 예시 |
부록 B. 헷갈리는 개념 비교표
| A | B | 구분 포인트 |
|---|---|---|
| 필수 결합 | 우연 결합 | 필수는 목적상 필요하고, 우연은 설계 실수로 생긴 연결이다. |
| 공유 수명주기 | 공유 지식 | 앞은 함께 배포되는 정도, 뒤는 서로 속사정을 아는 정도다. |
부록 C. 더 읽을 자료
- 이 장의
더 해보기섹션 — 이미 모아 둔 공식 문서나 실습 링크가 있으면 여기서 먼저 확인한다. - 같은 책의
0장 한눈에 보기— 용어가 막히면 0장의 용어집과 개념 척추로 돌아간다. - 원본 딥다이브판 같은 장 — 입문판을 읽고 큰 흐름이 잡힌 뒤 세부 논리를 더 깊게 확인한다.
- 이 장의
flashcards.json— 읽은 직후 질문만 보고 답을 떠올리는 회상 연습에 쓴다.
부록 D. 연습문제 풀이
- 설명 예시.
결합은 적이 아니라 접착제다는 변경이 어디로 번지는지 보고, 필요한 연결과 줄여야 할 연결을 구분하게 해 주는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다. - 구분 예시. 두 개념(
필수 결합,우연 결합)의 차이는 이렇게 잡으면 된다. 필수는 목적상 필요하고, 우연은 설계 실수로 생긴 연결이다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다. - 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.
클릭하거나 Space를 눌러 뒤집기